Skip to main content

Basic types

Field numbers

Are used to identify position inside the string byte. int32 id = 1; is not an asignment

Scalar data types

ProtobufC#Notes
doubledouble
floatfloat
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.
int64long
int64
uint32
uint64
sint32
sint64
fixed32
fixed64
sfixed32int
sfixed64int
boolbool
stringstring
bytesByteString
  1. The standard encoding for int32 and int64 is inefficient when you’re working with signed values. If your field is likely to contain negative numbers, use sint32 or sint64 instead. These types map to the C# int and long types, respectively.
  2. The fixed fields always use the same number of bytes no matter what the value is. This behavior makes serialization and deserialization faster for larger values.
  3. Protobuf strings are UTF-8 (or 7-bit ASCII) encoded. The encoded length can’t be greater than 232.
  4. The Protobuf runtime provides a ByteString type that maps easily to and from C# byte[] arrays

Enums

An enum must contain a constant that maps to zero as its first element. This is because:

  • There must be a zero value, so that we can use 0 as a numeric default value.
  • The zero value needs to be the first element, for compatibility with the proto2 semantics where the first enum value is always the default.

Enum values must be in the 32 bit integer range. If you need negative values, better use messages.

enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}

When using it inside the definition:

 message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
Corpus corpus = 4;
}

Note that the .proto language allows multiple enum symbols to have the same numeric value. Symbols with the same numeric value are synonyms. These are represented in C# in exactly the same way, with multiple names corresponding to the same numeric value.

Other .NET primitives

Some of the C# types are not supported by default, but some of them are covered by the Google’s Well Known Types.

Well Known types - are types in the google.protobuf package API, provide code generation and runtime support for complex field types across the supported platforms

Decimals

Protobuf doesn't natively support the .NET decimal type, just double and float. There's an ongoing discussion in the Protobuf project about the possibility of adding a standard Decimal type to the well-known types, with platform support for languages and frameworks that support it. Nothing has been implemented yet.

It's possible to create a message definition to represent the decimal type that would work for safe serialization between .NET clients and servers. But developers on other platforms would have to understand the format being used and implement their own handling for it.

Creating a custom decimal type for Protobuf

A simple implementation might be similar to the nonstandard Money type that some Google APIs use, without the currency field.

package CustomTypes;

// Example: 12345.6789 -> { units = 12345, nanos = 678900000 }
message DecimalValue {

// Whole units part of the amount
int64 units = 1;

// Nano units of the amount (10^-9)
// Must be same sign as units
sfixed32 nanos = 2;
}

The nanos field represents values from 0.999_999_999 to -0.999_999_999. For example, the decimal value 1.5m would be represented as { units = 1, nanos = 500_000_000 }. This is why the nanos field in this example uses the sfixed32 type, which encodes more efficiently than int32 for larger values. If the units field is negative, the nanos field should also be negative.

[!NOTE] There are multiple other algorithms for encoding decimal values as byte strings, but this message is easier to understand than any of them. The values are not affected by endianness on different platforms.

Conversion between this type and the BCL decimal type might be implemented in C# like this:

namespace CustomTypes;
public partial class DecimalValue
{
private const decimal NanoFactor = 1_000_000_000;
public DecimalValue(long units, int nanos)
{
Units = units;
Nanos = nanos;
}

public static implicit operator decimal(CustomTypes.DecimalValue grpcDecimal)
{
return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor;
}

public static implicit operator CustomTypes.DecimalValue(decimal value)
{
var units = decimal.ToInt64(value);
var nanos = decimal.ToInt32((value - units) * NanoFactor);
return new CustomTypes.DecimalValue(units, nanos);
}
}

[!IMPORTANT] Whenever you use custom message types like this, you must document them with comments in .proto. Other developers can then implement conversion to and from the equivalent type in their own language or framework.

Nullable types

The Protobuf code generation for C# uses the native types, such as int for int32. So the values are always included and can’t be null.

syntax = "proto3"
import "google/protobuf/wrappers.proto"
message Person {
google.protobuf.Int32Value age = 5;
}

Protobuf will use the simple T? (for example, int?) for the generated message property. The following table shows the complete list of wrapper types with their equivalent C# type:

C# typeWell Known Type wrapper
double?google.protobuf.DoubleValue
float?google.protobuf.FloatValue
int?google.protobuf.Int32Value
long?google.protobuf.Int64Value
uint?google.protobuf.UInt32Value
ulong?google.protobuf.UInt64Value

Date & times

syntax = "proto3"
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";

message Meeting {
string subject = 1;
google.protobuf.Timestamp time = 2;
google.protobuf.Duration duration = 3;
}

// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan
var meeting = new Meeting
{
Time = Timestamp.FromDateTimeOffset(meetingTime),
Duration = Duration.FromTimeSpan(meetingLength)
};
// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan
DateTimeOffset time = meeting.Time.ToDateTimeOffset();
TimeSpan? duration = meeting.Duration?.ToTimeSpan()
Offset

The Timestamp type works with UTC times. DateTimeOffset values always have an offset of zero, and the DateTime.Kind property is always DateTimeKind.Utc.

The well-known types Timestamp and Duration are represented in .NET as classes.

C# typeProtobuf well-known type
DateTimeOffsetgoogle.protobuf.Timestamp
DateTimegoogle.protobuf.Timestamp
TimeSpangoogle.protobuf.Duration

Guids

The Guid type, also known as UUID on other platforms, is not directly supported by Protobuf. As a result, there is no recognized type for it. The recommended method is to treat Guid values as a string field and use the standard 8-4-4-4-12 hexadecimal format, such as 45a9fda3-bd01-47a9-8460-c1cd7484b0b3, which can be parsed by all languages and platforms. It is not advisable to use a bytes field for Guid values as it can cause unexpected behavior when Protobuf interacts with other platforms like Java due to endianness issues.

Any

Any message type lets you use messages as embedded types without having their .proto definition. An Any contains an arbitrary serialized message as bytes, along with a URL that acts as a globally unique identifier for and resolves to that message’s type. To use the Any type, you need to import google/protobuf/any.proto.

Title
import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}